﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using System.Windows.Forms;
using System.ComponentModel;
using System.IO;

namespace XP_More
{
	class Duplicator
	{
		private BackgroundWorker m_worker;
		private DoWorkEventArgs m_eventArgs;
		private int m_stageBegin, m_stageEnd;
		private List<string> m_filesCreated;

		// Initialize the duplicator with the background worker details,
		// so that it can communicate its progress and be stopped if needed.
		public Duplicator(BackgroundWorker worker, DoWorkEventArgs eventArgs)
		{
			m_worker = worker;
			m_eventArgs = eventArgs;
			m_filesCreated = new List<string>();
		}

		// Duplicate the VM described by src to trg
		public bool Duplicate(VmInfo src, VmInfo trg)
		{
			bool copied;

			pbCancel = 0;
			SetStageRange(0, 5);
			copied = XCopy(src.Vmcx, trg.Vmcx);
			m_filesCreated.Add(trg.Vmcx);
			if (pbCancel == 1)
			{
				Cleanup();
				return false;
			}
			SetStageRange(5, 10);
			copied = XCopy(src.Vmc, trg.Vmc);
			m_filesCreated.Add(trg.Vmc);
			if (pbCancel == 1)
			{
				Cleanup();
				return false;
			}
			int vhdRange = GetVhdRange(85, src);
			SetStageRange(10, 10 + vhdRange);
			copied = XCopy(src.Vhd, trg.Vhd);
			m_filesCreated.Add(trg.Vhd);
			if (pbCancel == 1)
			{
				Cleanup();
				return false;
			}
			if (vhdRange != 85)
			{
				SetStageRange(10 + vhdRange, 95);
				copied = XCopy(src.Vsv, trg.Vsv);
				m_filesCreated.Add(trg.Vsv);
				if (pbCancel == 1)
				{
					Cleanup();
					return false;
				}
			}
			UpdateNewVm(trg);
			m_worker.ReportProgress(100);
			if (pbCancel == 1)
			{
				Cleanup();
				return false;
			}
			return true;
		}

		public void CancelDup()
		{
			pbCancel = 1;
		}

		private void Cleanup()
		{
			foreach (string file in m_filesCreated)
			{
				try
				{
					File.Delete(file);
				}
				catch (Exception /*ex*/)
				{
					// guess we're stuck with an undeletable file...
				}
			}
		}

		private int GetVhdRange(int totalRange, VmInfo srcVm)
		{
			if (!File.Exists(srcVm.Vsv))
				return totalRange;
			double vhdSize = new FileInfo(srcVm.Vhd).Length;
			double vsvSize = new FileInfo(srcVm.Vsv).Length;
			double vhdRange = totalRange * (vhdSize / (vhdSize + vsvSize));
			return (int)vhdRange;
		}

		private void SetStageRange(int begin, int end)
		{
			m_stageBegin = begin;
			m_stageEnd = end;
		}

		private void UpdateNewVm(VmInfo newVm)
		{
			UpdateVmcx(newVm);
			UpdateVcm(newVm);
		}

		private void UpdateVcm(VmInfo newVm)
		{
			// set the vhd entry
			string[] xVhdPath = { /*"preferences",*/ "hardware", "pci_bus",
							 "ide_adapter", "ide_controller", "location",
							 "pathname", "absolute" };
			XDocument xmlDoc = XDocument.Load(newVm.Vmc);
			XElement elem = xmlDoc.Root;
			foreach (string tag in xVhdPath)
			{
				elem = elem.Element(tag);
				if (elem == null)
				{
					throw new Exception("Element " + tag + " (VHD path) not found in VMC file " +
						newVm.Vmc + ". Is it valid?");
				}
			}
			elem.Value = newVm.Vhd;
			elem.Parent.Element("relative").Value = ".\\" + newVm.Name + ".vhd";
			// set the MAC address entry
			string[] xMacPath = { /*"preferences",*/ "hardware", "pci_bus",
							 "ethernet_adapter", "ethernet_controller",
							 "ethernet_card_address" };
			elem = xmlDoc.Root;
			foreach (string tag in xMacPath)
			{
				elem = elem.Element(tag);
				if (elem == null)
				{
					throw new Exception("Element " + tag + " (MAC address) not found in VMC file " +
						newVm.Vmc + ". Is it valid?");
				}
			}
			elem.Value = "";	// make empty so it will be regenerated next time
			// set the vsv entry, if exists
			if (newVm.Vsv.Length > 0)
			{
				string[] xVsvPath = { /*"preferences",*/ "settings", "configuration",
							 "saved_state", "path", "absolute" };
				elem = xmlDoc.Root;
				foreach (string tag in xVsvPath)
				{
					elem = elem.Element(tag);
					if (elem == null)
					{
						break;	// vsv might accidently exist as a leftover. ignore it then.
					}
				}
				if (elem != null)
				{
					elem.Value = newVm.Vsv;
					elem.Parent.Element("relative").Value = ".\\" + newVm.Name + ".vsv";
				}
			}
			// save the changes
			xmlDoc.Save(newVm.Vmc);
		}

		private void UpdateVmcx(VmInfo newVm)
		{
			XDocument xmlDoc = XDocument.Load(newVm.Vmcx);
			XElement vmDescr, elem;

			vmDescr = xmlDoc.Element("vm_description");

			elem = vmDescr.Element("primary_disk1");
			if (elem == null)
			{
				throw new Exception("Primary disk setting not found in VMCX file " +
					newVm.Vmcx + ". Is it valid?");
			}
			elem.Value = newVm.Vhd;
			elem = vmDescr.Element("vmc_path");
			if (elem == null)
			{
				throw new Exception("VMC path setting not found in VMCX file " +
					newVm.Vmcx + ". Is it valid?");
			}
			elem.Value = newVm.Vmc;

			xmlDoc.Save(newVm.Vmcx);
		}

		// CopyFileEx stuff taken from http://www.pinvoke.net/default.aspx/kernel32.CopyFileEx
		#region CopyFileEx

		[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
		[return: MarshalAs(UnmanagedType.Bool)]
		static extern bool CopyFileEx(string lpExistingFileName, string lpNewFileName,
		   CopyProgressRoutine lpProgressRoutine, IntPtr lpData, ref Int32 pbCancel,
		   CopyFileFlags dwCopyFlags);

		delegate CopyProgressResult CopyProgressRoutine(
		long TotalFileSize,
		long TotalBytesTransferred,
		long StreamSize,
		long StreamBytesTransferred,
		uint dwStreamNumber,
		CopyProgressCallbackReason dwCallbackReason,
		IntPtr hSourceFile,
		IntPtr hDestinationFile,
		IntPtr lpData);

		int pbCancel;

		enum CopyProgressResult : uint
		{
			PROGRESS_CONTINUE = 0,
			PROGRESS_CANCEL = 1,
			PROGRESS_STOP = 2,
			PROGRESS_QUIET = 3
		}

		enum CopyProgressCallbackReason : uint
		{
			CALLBACK_CHUNK_FINISHED = 0x00000000,
			CALLBACK_STREAM_SWITCH = 0x00000001
		}

		[Flags]
		enum CopyFileFlags : uint
		{
			COPY_FILE_FAIL_IF_EXISTS = 0x00000001,
			COPY_FILE_RESTARTABLE = 0x00000002,
			COPY_FILE_OPEN_SOURCE_FOR_WRITE = 0x00000004,
			COPY_FILE_ALLOW_DECRYPTED_DESTINATION = 0x00000008
		}

		private bool XCopy(string oldFile, string newFile)
		{
			return CopyFileEx(oldFile, newFile, new CopyProgressRoutine(this.CopyProgressHandler), IntPtr.Zero, ref pbCancel, CopyFileFlags.COPY_FILE_RESTARTABLE);
		}

		#endregion

		private CopyProgressResult CopyProgressHandler(long total, long transferred, long streamSize, long StreamByteTrans, uint dwStreamNumber, CopyProgressCallbackReason reason, IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData)
		{
			if (m_worker.CancellationPending)
			{
				m_eventArgs.Cancel = true;
				pbCancel = 1;
				return CopyProgressResult.PROGRESS_CANCEL;
			}

			double stageDone = (double)transferred / total;
			double totalDone = m_stageBegin + (m_stageEnd - m_stageBegin) * stageDone;
			m_worker.ReportProgress((int)(totalDone));

			return CopyProgressResult.PROGRESS_CONTINUE;
		}
	}
}
